// MainDlg.cpp : implementation of the CMainDlg class
//
/////////////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "resource.h"

#include <algorithm>
#include <functional>
#include <cstdlib>
#include "MainDlg.h"

namespace {

static const GUID g_SerialPortGuid = {
	0x4D36E978, 0xE325, 0x11CE, 0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18 
};

DWORD GetVersion(const std::wstring &dll_name) {
	HINSTANCE hinstance;
	DWORD version = 0;

	hinstance = ::LoadLibrary(dll_name.c_str());
	if (hinstance != NULL) {
		DLLGETVERSIONPROC _GetVersion = reinterpret_cast<DLLGETVERSIONPROC>( \
			::GetProcAddress(hinstance, "DllGetVersion"));

		if (_GetVersion) {
			DLLVERSIONINFO2 dll_info = { 0 };
			dll_info.info1.cbSize = sizeof(DLLVERSIONINFO);
			if (SUCCEEDED(_GetVersion( \
				reinterpret_cast<DLLVERSIONINFO*>(&dll_info)))) \
				version = MAKELONG(dll_info.info1.dwMinorVersion, \
					dll_info.info1.dwMajorVersion);
		}
		::FreeLibrary(reinterpret_cast<HMODULE>(hinstance));
	}
	return version;
}

DWORD GetNotifyIconDataSize() {
	DWORD shell32_ver = GetVersion(TEXT("C://Windows//System32//Shell32.dll"));
	if (shell32_ver >= 0x00060001)
		return sizeof(NOTIFYICONDATA);
	// Windows XP
	else if (shell32_ver >= 0x00050100)
		return NOTIFYICONDATA_V3_SIZE;
	// Windows 2000
	else if (shell32_ver >= 0x00050000)
		return NOTIFYICONDATA_V2_SIZE;
	// Windows version lower than Windows 2000
	return NOTIFYICONDATA_V1_SIZE;
}

struct SortByName : 
	public std::binary_function<const USB_DevInfo &, const USB_DevInfo &, bool> {
	inline result_type operator()(first_argument_type dev1,
		second_argument_type dev2) const throw() {
		// "COM" length = 3
		ATLASSERT(dev1.name.length() > 3 && dev2.name.length() > 3);
		return _tstol(&dev1.name[3]) < _tstol(&dev2.name[3]);
	}
};

struct FindByName : public std::unary_function<const USB_DevInfo &, bool> {
	explicit inline FindByName(const TCHAR *name) 
		: m_name(name) {
	}

	inline result_type operator()(argument_type dev) const throw() {
		return (dev.name.compare(m_name) == 0);
	}

	std::wstring m_name;
};

bool GetDeviceRegistryName(const CString &reg_name, CString &name, const HKEY hkey)
{
	TCHAR name_buf[MAX_PATH] = {0};
	DWORD name_buf_size = sizeof(name_buf);
	DWORD type = 0;

	if ((::RegQueryValueEx(hkey, 
		reg_name.GetString(), 
		0,
		&type,
		reinterpret_cast<LPBYTE>(&name_buf[0]), 
		&name_buf_size) == ERROR_SUCCESS) && (type == REG_SZ)) {
		name.SetString(name_buf);
		return true;
	}

	return false;
}

bool GetDeviceRegistryProperty(CString &description,
	DWORD property_flag,
	const USB_DevInfo* usb_dev_info) {
	TCHAR description_buffer[MAX_PATH] = { 0 };
	DWORD data_type = 0;
	DWORD need_size = 0;

	if (::SetupDiGetDeviceRegistryProperty(usb_dev_info->hdevinfo,
		const_cast<SP_DEVINFO_DATA*>(&usb_dev_info->spdevinfo_data), 
		property_flag, 
		&data_type, 
		reinterpret_cast<PBYTE>(description_buffer), 
		sizeof(description_buffer),
		&need_size) != FALSE) {
		description.SetString(description_buffer);
		return true;
	}
	return false;
}

bool GetSerialDeviceInfo(std::vector<USB_DevInfo> &devinfo_list) {
	HDEVINFO hdevinfo = INVALID_HANDLE_VALUE;
	bool found = FALSE;
	DWORD count = 0;

	ATLTRACE(L"%s \n", L"GetDeviceRegistryProperty");

	hdevinfo = ::SetupDiGetClassDevs(&g_SerialPortGuid, NULL, NULL, DIGCF_PRESENT | DIGCF_PROFILE);
	if (hdevinfo == INVALID_HANDLE_VALUE)
		return false;

	do  {
		SP_DEVINFO_DATA sp_devinfo_data = { 0 };	
		sp_devinfo_data.cbSize = sizeof(SP_DEVINFO_DATA);
		for (DWORD i = 0 ; found = (::SetupDiEnumDeviceInfo(hdevinfo, i, &sp_devinfo_data) != FALSE) ; ++i) {
			HKEY hkey = ::SetupDiOpenDevRegKey(hdevinfo, 
				&sp_devinfo_data, 
				DICS_FLAG_GLOBAL,
				0, 
				DIREG_DEV, 
				KEY_QUERY_VALUE);

			if (hkey != NULL) {
				CString port_name;
				CString str;
				if (GetDeviceRegistryName(TEXT("PortName"), port_name, hkey)) {
					if (port_name.Find(TEXT("COM")) != -1) {
						devinfo_list.push_back(USB_DevInfo(hdevinfo, 
							sp_devinfo_data, 
							hkey, 
							port_name.GetBuffer()));
						ATLTRACE(L"Find %s \n", port_name.GetBuffer());
					}
				}
			} else {
				break;
			}
		}
	} while (found != false);

	return devinfo_list.size() ? true : false;
}

class AboutDlg : public CDialogImpl<AboutDlg> {
private:
	CHyperLink email_link_;

public:
	enum { IDD = IDD_ABOUTBOX };

	BEGIN_MSG_MAP_EX(AboutDlg)
		MSG_WM_INITDIALOG(OnInitDialog)
		COMMAND_ID_HANDLER_EX(IDCANCEL, OnCancel)
		COMMAND_ID_HANDLER_EX(IDOK, OnOK)
	END_MSG_MAP()

	LRESULT OnInitDialog(HWND hWnd, LPARAM lParam)
	{
		CenterWindow();
		email_link_.SubclassWindow(GetDlgItem(IDC_EMAILL));
		email_link_.SetLabel(TEXT("Project Homepage"));
		email_link_.SetHyperLink(TEXT("http://code.google.com/p/serial-port-notifier/"));
		return TRUE;
	}

	void OnOK(UINT uNotifyCode, int nID, HWND hWndCtl)
	{
		EndDialog(nID);
	}

	void OnCancel(UINT uNotifyCode, int nID, HWND hWndCtl)
	{
		EndDialog(nID);
	}

};

}

void CMainDlg::getSysPort()
{
	if (GetSerialDeviceInfo(usb_dev_list_)) {
		// Sort by number
		std::sort(usb_dev_list_.begin(), usb_dev_list_.end(), SortByName());

		HICON icon = ::LoadIcon(ModuleHelper::GetResourceInstance(),
			MAKEINTRESOURCE(IDI_ICON1));
		ATLASSERT(icon != NULL);

		BOOL ret;
		ICONINFO icon_info;

		ret = ::GetIconInfo(icon, &icon_info);
		ATLASSERT(ret == TRUE);

		ret = icon_list_.Create(icon_info.yHotspot * 2,
			icon_info.yHotspot * 2, 
			ILC_COLOR32,
			0, 
			static_cast<DWORD>(usb_dev_list_.size()));

		ATLASSERT(ret == TRUE);

		for (std::vector<USB_DevInfo>::size_type i = 0 ; i < usb_dev_list_.size() ; ++i)
			icon_list_.AddIcon(icon);

		port_combo_.SetImageList(icon_list_);

		for (std::vector<USB_DevInfo>::size_type i = 0 ; i < usb_dev_list_.size() ; ++i) {
			insertPortItem(usb_dev_list_[i].name,
				static_cast<std::vector<USB_DevInfo>::size_type>(i), 0);
			usb_dev_list_[i].index = static_cast<int>(i);
		}

		port_combo_.SetCurSel(0);
	}
}

LRESULT CMainDlg::OnSelectChange(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
	if (usb_dev_list_.size() > 0) {
		CString str;
		int sel = port_combo_.GetCurSel();
		if (GetDeviceRegistryProperty(str, SPDRP_MFG, &usb_dev_list_[sel]))
			mfg_edit_.SetWindowText(str);
		CString tmp;
		port_combo_.GetWindowText(tmp);
	}
	return 0;
}

int CMainDlg::insertPortItem(const std::wstring &str, int num_item, int num_icon) 
{
	COMBOBOXEXITEM item = { 0 };
	item.mask = CBEIF_TEXT | CBEIF_IMAGE | CBEIF_SELECTEDIMAGE;
	item.pszText = const_cast<LPTSTR>(str.c_str());
	item.cchTextMax = static_cast<int>(str.length());
	item.iItem = num_item;
	item.iImage = num_icon;
	item.iSelectedImage = num_icon;
	return port_combo_.InsertItem(&item);
}


LRESULT CMainDlg::OnDeviceChange(UINT nEventType, DWORD dwData)
{
	const DEV_BROADCAST_HDR *broadcast_hdr = \
		reinterpret_cast<const DEV_BROADCAST_HDR*>(dwData);
	const DEV_BROADCAST_PORT *broadcast_port = NULL;
	CString cur_name;
	int cur_sel = -1;
	switch (nEventType) {
	case DBT_DEVICEREMOVECOMPLETE:
		// Device is gone
		cur_sel = port_combo_.GetCurSel();
		if (cur_sel != CB_ERR)
			port_combo_.GetLBText(cur_sel, cur_name);
		if (broadcast_hdr != NULL && broadcast_hdr->dbch_devicetype == DBT_DEVTYP_PORT) {
			broadcast_port = reinterpret_cast<const DEV_BROADCAST_PORT*>(dwData);
			if (broadcast_port != NULL) {
				std::vector<USB_DevInfo>::iterator itr = std::find_if(usb_dev_list_.begin(),
					usb_dev_list_.end(), FindByName(broadcast_port->dbcp_name));
				std::vector<USB_DevInfo>::iterator erase_itr = itr;
				if (itr != usb_dev_list_.end()) {
					ATLTRACE(L"Delete %s \n", (*itr).name.c_str());
					showBalloonTip(TEXT("Disconnected"), (*itr).name, 10, NIIF_INFO);
					// User was already remove the COM port
					int err = port_combo_.DeleteItem((*itr).index);
					// If remove current select item, just select first item (0)
					if ((*itr).index == cur_sel)
						cur_sel = 0;
					ATLASSERT(err != CB_ERR);
					// Item was been deleted, index must be updata
					while (itr != usb_dev_list_.end()) {
						--((*itr).index);
						++itr;
					}
					usb_dev_list_.erase(erase_itr);
					if (cur_sel != 0) {
						itr = std::find_if(usb_dev_list_.begin(),
							usb_dev_list_.end(), FindByName(cur_name));
						cur_sel = (*itr).index;
					}
					port_combo_.SetCurSel(cur_sel);
				}
			}
		}
		break;
	case DBT_DEVICEARRIVAL:
		// System detected a new device
		cur_sel = port_combo_.GetCurSel();
		if (cur_sel != CB_ERR)
			port_combo_.GetLBText(cur_sel, cur_name);
		if (broadcast_hdr != NULL && broadcast_hdr->dbch_devicetype == DBT_DEVTYP_PORT) {
			broadcast_port = reinterpret_cast<const DEV_BROADCAST_PORT*>(dwData);
			if (broadcast_port != NULL) {
				std::vector<USB_DevInfo>::iterator itr = std::find_if(usb_dev_list_.begin(),
					usb_dev_list_.end(), FindByName(broadcast_port->dbcp_name));
				if (itr == usb_dev_list_.end()) {
					showBalloonTip(TEXT("Connected"), broadcast_port->dbcp_name, 10, NIIF_INFO);
					port_combo_.ResetContent();
					icon_list_.Destroy();
					usb_dev_list_.erase(usb_dev_list_.begin(), usb_dev_list_.end());
					getSysPort();
					if (cur_sel != CB_ERR) {
						itr = std::find_if(usb_dev_list_.begin(),
							usb_dev_list_.end(), FindByName(cur_name));
						cur_sel = (*itr).index;
					}
				}
			}
		}
		if (cur_sel != CB_ERR)
			port_combo_.SetCurSel(cur_sel);
		else
			port_combo_.SetCurSel(0);
		break;
	}

	// Updata to mfg text edit
	int sel = port_combo_.GetCurSel();
	if (sel != CB_ERR) {
		CString str;
		if (GetDeviceRegistryProperty(str, SPDRP_MFG, &usb_dev_list_[sel]))	
			mfg_edit_.SetWindowText(str);
	}
	return 0;
}

LRESULT CMainDlg::OnInitDialog(CWindow wndFocus, LPARAM lInitPara)
{
	// center the dialog on the screen
	CenterWindow();

	// set icons
	HICON hIcon = (HICON)::LoadImage(_Module.GetResourceInstance(), MAKEINTRESOURCE(IDR_MAINFRAME), 
		IMAGE_ICON, ::GetSystemMetrics(SM_CXICON), ::GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR);
	SetIcon(hIcon, TRUE);
	HICON hIconSmall = (HICON)::LoadImage(_Module.GetResourceInstance(), MAKEINTRESOURCE(IDR_MAINFRAME), 
		IMAGE_ICON, ::GetSystemMetrics(SM_CXSMICON), ::GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR);
	SetIcon(hIconSmall, FALSE);

	port_combo_.Attach(GetDlgItem(IDC_COMCOMBO));
	mfg_edit_.Attach(GetDlgItem(IDC_EDIT1));
	always_on_top_check_.Attach(GetDlgItem(IDC_CHECK_ALWAYS_ON_TOP));
	minize_check_.Attach(GetDlgItem(IDC_CHECK_MINIMIZE));

	memset(&notify_icon_data_, 0, sizeof(NOTIFYICONDATA));

	is_topmost_ = false;
	is_minimize_active_ = false;
	is_minimize_ = false;

	DEV_BROADCAST_DEVICEINTERFACE notify_filter = { 0 };

	notify_filter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
	notify_filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
	notify_filter.dbcc_classguid = g_SerialPortGuid;

	// Register device notification
	dev_notify_handle_ = ::RegisterDeviceNotification(m_hWnd, 
		&notify_filter, DEVICE_NOTIFY_WINDOW_HANDLE);

	ATLASSERT(dev_notify_handle_ != NULL);

	getSysPort();

	if (usb_dev_list_.size()) {
		CString str;
		if (GetDeviceRegistryProperty(str, SPDRP_MFG, &usb_dev_list_[0]))
			mfg_edit_.SetWindowText(str);
		else
			mfg_edit_.SetWindowText(TEXT("Empty"));
	}
	return TRUE;
}

LRESULT CMainDlg::OnAppAbout(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
	AboutDlg dlg;
	dlg.DoModal();
	return 0;
}

LRESULT CMainDlg::OnOK(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
	// TODO: Add validation code 
	EndDialog(wID);
	return 0;
}

LRESULT CMainDlg::OnCancel(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
	EndDialog(wID);

	if (dev_notify_handle_ != NULL)
		::UnregisterDeviceNotification(dev_notify_handle_);

	if (notify_icon_data_.cbSize)
		::Shell_NotifyIcon(NIM_DELETE, &notify_icon_data_);

	return 0;
}

void CMainDlg::OnAlwaysOnTop(UINT uNotifyCode, int nID, HWND hWndCtl)
{
	HWND top_hwnd = NULL;
	if (!is_topmost_ && always_on_top_check_.GetCheck()) {
		is_topmost_ = true;
		top_hwnd = HWND_TOPMOST;
	} else {
		is_topmost_ = false;
		top_hwnd = HWND_NOTOPMOST;
	}
	SetWindowPos(top_hwnd, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}

LRESULT CMainDlg::OnSysCommand(UINT nCommand, CPoint)
{
	if (is_minimize_active_) {
		switch (nCommand) {
		case SC_MINIMIZE:
			if(!notify_icon_data_.cbSize) {
				notify_icon_data_.cbSize = GetNotifyIconDataSize();
				notify_icon_data_.hWnd = m_hWnd;
				notify_icon_data_.uID = 1;
				notify_icon_data_.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_INFO;
				notify_icon_data_.uCallbackMessage = WM_SYSTEMTRAYICON;
				notify_icon_data_.hIcon = (HICON)::LoadImage(\
					_Module.GetResourceInstance(), MAKEINTRESOURCE(IDR_MAINFRAME),\
					IMAGE_ICON, ::GetSystemMetrics(SM_CXSMICON), ::GetSystemMetrics(SM_CYSMICON),\
					LR_DEFAULTCOLOR);
				CString sWindowText;
				GetWindowText(sWindowText);
				_tcscpy_s(notify_icon_data_.szTip, sWindowText);
				if(!::Shell_NotifyIcon(NIM_ADD, &notify_icon_data_)) {
					SetMsgHandled(FALSE);
					return 0;
				}
			}
			ShowWindow(SW_HIDE);
			is_minimize_ = true;
			return 0;
		}
	}
	SetMsgHandled(FALSE);
	return 0;
}

LRESULT CMainDlg::OnScRestore(UINT, INT, HWND)
{
	if (notify_icon_data_.cbSize) {
		::Shell_NotifyIcon(NIM_DELETE, &notify_icon_data_);
		memset(&notify_icon_data_, 0, sizeof(NOTIFYICONDATA));
	}
	ShowWindow(SW_SHOW);
	BringWindowToTop();
	return 0;
}

LRESULT CMainDlg::OnSystemTrayIcon(UINT, WPARAM wParam, LPARAM lParam)
{
	switch (lParam) {
	case WM_LBUTTONDBLCLK:
		SendMessage(WM_COMMAND, SC_RESTORE);
		break;
	case WM_RBUTTONUP:
		::SetForegroundWindow(m_hWnd);
		CMenuHandle menu = GetSystemMenu(FALSE);
		menu.EnableMenuItem(SC_RESTORE, MF_BYCOMMAND | MF_ENABLED);
		menu.EnableMenuItem(SC_MOVE, MF_BYCOMMAND | MF_GRAYED);
		menu.EnableMenuItem(SC_SIZE, MF_BYCOMMAND | MF_GRAYED);
		menu.EnableMenuItem(SC_MINIMIZE, MF_BYCOMMAND | MF_GRAYED);
		menu.EnableMenuItem(SC_MAXIMIZE, MF_BYCOMMAND | MF_GRAYED);
		menu.EnableMenuItem(SC_CLOSE, MF_BYCOMMAND | MF_ENABLED);
		CPoint pos;
		ATLVERIFY(GetCursorPos(&pos));
		menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_BOTTOMALIGN, pos.x, pos.y, m_hWnd);
		break;
	}
	return 0;
}

void CMainDlg::OnMinimize(UINT uNotifyCode, int nID, HWND hWndCtl)
{
	if (!is_minimize_active_ && minize_check_.GetCheck()) {
		is_minimize_active_ = true;
	} else {
		is_minimize_active_ = false;
	}
	if (is_minimize_active_)
		SendMessage(WM_SYSCOMMAND, SC_MINIMIZE, 0);
}

LRESULT CMainDlg::OnScClose(UINT, INT, HWND)
{
	SendMessage(WM_COMMAND, IDCANCEL);
	return 0;
}

bool CMainDlg::setTooltipText(const std::wstring &tooltip_text)
{
	if (is_minimize_active_) {
		// Fill the structure
		notify_icon_data_.uFlags = NIF_TIP;
		_tcscpy_s(notify_icon_data_.szTip, tooltip_text.c_str());
		// Set
		return ::Shell_NotifyIcon(NIM_MODIFY, &notify_icon_data_) != FALSE;
	}
	return false;
}

bool CMainDlg::showBalloonTip(const std::wstring &tittle,
	const std::wstring &balloontip_text, 
	UINT timeouts, 
	DWORD info_flag)
{
	if (is_minimize_active_ && is_minimize_) {
		notify_icon_data_.uFlags = NIF_INFO;
		notify_icon_data_.uTimeout = timeouts;
		notify_icon_data_.dwInfoFlags = info_flag;
		_tcscpy_s(notify_icon_data_.szInfo, balloontip_text.c_str());
		_tcscpy_s(notify_icon_data_.szInfoTitle, !tittle.empty() ? tittle.c_str() : TEXT(" "));
		return ::Shell_NotifyIcon(NIM_MODIFY, &notify_icon_data_) != FALSE;
	}
	return false;
}
